package com.qf.controller;

import com.alibaba.fastjson.JSON;
import com.qf.mapper.StudentMapper;
import com.qf.pojo.Student;
import com.qf.utils.ThreadUtiils;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@RestController
@RequestMapping("/stu")
public class StudentController implements InitializingBean {

    @Autowired
    private StudentMapper studentMapper;

    @Autowired
    private StringRedisTemplate redisTemplate;

    public static final String STU_CACHE_PREFIX = "student:id:";

    public static final String STU_LOCK_PREFIX = "LOCK:student:id:";

    @Autowired
    private RedissonClient client;

    //存放学生表中id的布隆过滤器
    private RBloomFilter<Integer> stuBloomFilter;

    /**
     * 这个方法在StudentController对象创建后，执行他的初始化流程时，会执行这个方法
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        stuBloomFilter = client.getBloomFilter("stuBloom");
        //对布隆过滤器进行容量初始化(参数一：期望存入的元素个数，参数二：期望的错误率)
        stuBloomFilter.tryInit(1000,0.01);

        //向布隆过滤器中存入的学生表的id
        List<Integer> ids = studentMapper.findIds();
        for (Integer id : ids) {
            stuBloomFilter.add(id);
        }
    }

    @GetMapping("/{id}")
    public Student findById(@PathVariable("id") Integer id){
        //先判断布隆过滤器中是否存在这个id值
        if (stuBloomFilter.contains(id)) {

            //等待缓存过期，模拟缓存击穿问题
            ThreadUtiils.sleep(8000);

            //可能存在
            //① 先查询缓存
            String stuJson = redisTemplate.opsForValue().get(STU_CACHE_PREFIX + id);
            if (stuJson != null) {
                //缓存中有数据
                System.out.println("命中缓存....");
                Student student = JSON.parseObject(stuJson, Student.class);
                return student;
            } else {

                //添加一个分布式锁，解决缓存击穿问题，保证缓存过期时，每次只有一个请求打到数据库
                RLock lock = client.getLock(STU_LOCK_PREFIX + id);
                try {
                    lock.lock(); //如果获取到锁，就会向下执行，如果没有获取到锁，就会在这个地方阻塞

                    //再次获取缓存 -- 双重校验
                    String jsonCache = redisTemplate.opsForValue().get(STU_CACHE_PREFIX + id);
                    if (jsonCache != null) {
                        //缓存中有数据
                        System.out.println("命中缓存....");
                        Student student = JSON.parseObject(jsonCache, Student.class);
                        return student;
                    }else {

                        //缓存中没有： ① 查询数据库 + ② 将数据存入缓存
                        System.out.println("缓存中没有，查询数据库....");
                        //② 从数据库查询
                        Student student = studentMapper.selectById(id);
                        if (student != null) {
                            //存入缓存
                            String json = JSON.toJSONString(student);
                            redisTemplate.opsForValue().set(STU_CACHE_PREFIX + id, json);
                        }/*else{
                                //缓存空对象: 会向redis服务器中存入很多无意义的对象，这些对象也是会占据redis服务器内存的。如果存入大量这种数据，最终redis服务器可能会宕机
                                String json = JSON.toJSONString(new Student());
                                redisTemplate.opsForValue().set(STU_CACHE_PREFIX + id,json);
                            }*/

                        return student;
                    }
                }finally {
                    //释放锁
                    lock.unlock();
                }
            }
        }else{
            //一定不存在
            System.out.println("数据库中不存在这个id的学生信息...");
            return null;
        }
    }
}
